/*
    Licensed to the Apache Software Foundation (ASF) under one
    or more contributor license agreements.  See the NOTICE file
    distributed with this work for additional information
    regarding copyright ownership.  The ASF licenses this file
    to you under the Apache License, Version 2.0 (the
    "License"); you may not use this file except in compliance
    with the License.  You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing,
    software distributed under the License is distributed on an
    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    KIND, either express or implied.  See the License for the
    specific language governing permissions and limitations
    under the License.
 */

package erwiki.api.parser;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.StringTokenizer;

import org.apache.log4j.Logger;
import org.jdom2.Attribute;

/**
 * Преобразует "расширенную" разметку ссылок в стиле JSPWiki в объект Link, содержащий текст ссылки,
 * гиперссылку и любые необязательные атрибуты ссылок (как атрибуты JDOM).
 * <p>
 * Парсер распознает три формы ссылок:
 * </p>
 * <ol>
 * <li><tt> [Text] </tt></li>
 * <li><tt> [Text | Link] </tt></li>
 * <li><tt> [Text | Link | attributes] </tt></li>
 * </ol>
 * где атрибуты разделены пробелами, в следующей в форме
 *
 * <pre>
 *      name1='value1' name2='value2' name3='value3' (и т.д.)
 * </pre>
 * <p>
 * Если анализ атрибута не удался, то парсер все равно вернет базовую ссылку, записав предупреждение
 * в журнал.
 * </p>
 *
 * <h3>Допустимые атрибуты</h3>
 * <p>
 * Атрибуты, которые не объявлены в <tt>&lt;a&gt;</tt> или те, которые допускают создание сценариев
 * в HTML (а это угроза безопасности) - игнорируются, не влияют на синтаксический анализ и не
 * отображаются в конечном списке атрибутов. Атрибуты 'href' и 'name' также игнорируются как
 * поддельные.</br>
 * Список разрешенных атрибутов: 'accesskey', 'charset', 'class', 'hreflang', 'id', 'lang', 'dir',
 * 'rel', 'rev', 'style', 'tabindex', 'target' , 'title', and 'type'.</br>
 * Объявленные атрибуты, которые будут игнорироваться: 'href', 'name', 'shape', 'coords', 'onfocus',
 * 'onblur', или любой другой 'on*' атрибут события.
 * </p>
 * <p>
 * Разрешенные атрибуты и значения целевых атрибутов представляют собой статические массивы строк.
 * ({@link #PERMITTED_ATTRIBUTES} и {@link #PERMITTED_TARGET_VALUES} соответственно), которые могут
 * быть изменены во время компиляции (т.е. предварительно объявлены).
 * </p>
 *
 * <h3>Разрешенные значения атрибута Target</h3>
 * <p>
 * Следующие целевые имена зарезервированы в HTML 4 и имеют особое значение. Это единственные
 * значения, разрешенные парсером.
 * <dl>
 * <dt><b>_blank</b></dt>
 * <dd>Пользовательский агент должен загрузить указанный документ в новое безымянное окно..</dd>
 * <dt><b>_self</b></dt>
 * <dd>Пользовательский агент должен загрузить документ в тот же фрейм, что и элемент, ссылающийся
 * на эту цель.</dd>
 * <dt><b>_parent</b></dt>
 * <dd>Пользовательский агент должен загрузить документ непосредственно в родительский элемент
 * FRAMESET текущего фрейма. Это значение эквивалентно _self, если текущий кадр не имеет
 * родителя.</dd>
 * <dt><b>_top</b></dt>
 * <dd>Пользовательский агент должен загрузить документ в полное исходное окно (таким образом
 * отменяя все остальные фреймы). Это значение эквивалентно _self, если текущий кадр не имеет
 * родителя.</dd>
 * </dl>
 *
 * <h3>Возвращаемое значение</h3>
 * <p>
 * Возвращается экземпляр <b>Link</b>, открытый внутренний класс с методами:
 * <ul>
 * <li><tt>getText()</tt> возвращает текст ссылки.</li>
 * <li><tt>getReference()</tt> возвращает значение гиперссылки.</li>
 * <li><tt>attributeCount()</tt> возвращает число объявленных атрибутов.</li>
 * <li><tt>getAttributes()</tt> возвращает итератор для любых проверенных XHTML-совместимых
 * атрибутов, возвращаемых как атрибуты JDOM.</li>
 * </ul>
 * <p>
 * Метод <tt>attributeCount()</tt> можно использовать для обхода вызова <tt>getAttributes()</tt>,
 * который создаст пустой итератор, а не вернет значение <code>null</code>, если атрибутов не будет.
 * </p>
 *
 * <h3>Пример: Форма ссылки 1</h3>
 * <p>
 * Из входящей ссылки викитекста:
 *
 * <pre>
 *     [Acme]
 * </pre>
 *
 * возвращает:
 *
 * <pre>
 *    getText():         "Acme"
 *    getReference():    "Acme"
 *    attributeCount():  0
 *    getAttributes():   пустой Iterator
 * </pre>
 *
 * <h3>Пример: Форма ссылки 2</h3>
 * <p>
 * Из входящей ссылки викитекста:
 *
 * <pre>
 *     [Acme | http://www.acme.com/]
 * </pre>
 *
 * возвращает:
 *
 * <pre>
 *    getText():         "Acme"
 *    getReference():    "http://www.acme.com/"
 *    attributeCount():  0
 *    getAttributes():   пустой Iterator
 * </pre>
 *
 * <h3>Пример: Форма ссылки 3</h3>
 * <p>
 * Из входящей ссылки викитекста:
 * </p>
 *
 * <pre>
 *    [Acme | http://www.acme.com/ | id='foo' rel='Next']
 * </pre>
 *
 * возвращает:
 *
 * <pre>
 *    getText():         "Acme"
 *    getReference():    "http://www.acme.com/"
 *    attributeCount():  2
 *    getAttributes():   Iterator содержит:
 *      JDOM Attribute:  id="foo"
 *      JDOM Attribute:  rel="Next"
 * </pre>
 */
public class LinkParser {
	private static Logger log = Logger.getLogger(LinkParser.class);

	/** Допустимые атрибуты ссылок. Массив следует держать отсортированным. */
	private static final String[] PERMITTED_ATTRIBUTES = new String[] { //
			"accesskey", "charset", "class", "dir", "hreflang", "id", "lang", //
			"rel", "rev", "style", "tabindex", "target", "title", "type" };

	/** Разрешенные значения атрибута 'target'. */
	private static final String[] PERMITTED_TARGET_VALUES = new String[] { "_blank", "_self", "_parent", "_top" };

	/**
	 * Ссылки с target="_blank" могут привести к проблемам с производительностью и безопасностью вашего
	 * сайта. Чтобы исправить это, добавьте к этим ссылкам rel="noopener" или rel="noreferrer".
	 */
	private static final String REL = "rel";
	private static final String NOREFERRER = "noreferrer";

	private static final String EQSQUO = "='";
	private static final String SQUO = "'";
	private static final String EQ = "=";
	private static final String TARGET = "target";
	private static final String DELIMS = " \t\n\r\f=";

	private static final List<Attribute> m_EMPTY = new ArrayList<>();

	/**
	 * Обрабатывает входящий текст вики-ссылки, выделяя текст ссылки, URI ссылки, а затем любые
	 * указанные атрибуты.
	 *
	 * @param linktext Текст вики-ссылки для анализа.
	 * @return Экземпляр Link, содержащий текст ссылки, референсную строку и любые допустимые атрибуты.
	 * @throws ParseException если параметр равен <code>null</code>.
	 */
	public Link parse(String linktext) throws ParseException {
		if (linktext == null) {
			throw new ParseException("null value passed to link parser");
		}

		Link link = null;

		try {
			// установить текст ссылки и референс ссылки.
			String[] parts = linktext.split("\\|", 3);
			if (parts.length == 1) {
				// ссылочная форма 1: [Acme]
				return new Link(parts[0].trim());
			} else if (parts.length == 2) {
				// ссылочная форма 2: [Acme | http://www.acme.com/]
				// text = Acme
				// ref = http://www.acme.com/
				return new Link(parts[0].trim(), parts[1].trim());
			}

			// ссылочная форма 3: [Acme | http://www.acme.com/ | id='foo' rel='Next']
			link = new Link(parts[0].trim(), parts[1].trim());
			// attribs = id='foo' rel='Next'
			String attribs = parts[2].trim();

			// Анализ атрибутов.
			// Присутствие "='" рассматривается как наличие спецификаций атрибутов.
			if (attribs.indexOf(EQSQUO) != -1) {
				try {
					StringTokenizer tok = new StringTokenizer(attribs, DELIMS, true);
					while (tok.hasMoreTokens()) {
						// получить токен имени атрибута
						String token = tok.nextToken(DELIMS).trim();
						while (isSpace(token) && tok.hasMoreTokens()) {
							// удалить все пробелы
							token = tok.nextToken(DELIMS).trim();
						}

						// поглотить '=', остановиться после '='
						require(tok, EQ);
						// поглотить открывающийся разделитель
						require(tok, SQUO);
						// получить токен значения, используя закрывающий разделитель
						String value = tok.nextToken(SQUO);
						// поглотить закрывающий разделитель
						require(tok, SQUO);

						if (token != null && value != null) {
							if (Arrays.binarySearch(PERMITTED_ATTRIBUTES, token) >= 0) {
								Attribute attr = new Attribute(token, value);
								link.addAttribute(attr);

								if (token.equals(TARGET)) {
									// _blank _self _parent _top
									if (Arrays.binarySearch(PERMITTED_TARGET_VALUES, value) < 0) {
										throw new ParseException(
												"unknown target`s attribute value='" + value + "' on link");
									}
									Attribute rel = new Attribute(REL, NOREFERRER);
									link.addAttribute(rel);
								}
							} else {
								throw new ParseException("unknown attribute name '" + token + "' on link");
							}
						} else {
							throw new ParseException("unable to parse link attributes '" + attribs + "'");
						}
					}
				} catch (ParseException pe) {
					log.warn("syntax error parsing link attributes '" + attribs + "': " + pe.getMessage());
				} catch (NoSuchElementException nse) {
					log.warn("expected more tokens while parsing link attributes '" + attribs + "'");
				}
			}
		} catch (Exception e) {
			log.warn(e.getClass().getName() + " thrown by link parser: " + e.getMessage());
		}

		return link;
	}

	private String require(StringTokenizer tok, String required) throws ParseException, NoSuchElementException {
		String s = tok.nextToken(required);
		if (!s.equals(required)) {
			throw new ParseException("expected '" + required + "' not '" + s + "'");
		}
		return s;
	}

	/**
	 * Возвращает <i>true</i>, если строка <tt>s</tt> полностью состоит из пробелов.
	 *
	 * @param s Строка для проверки.
	 * @return <i>True</i>, если "s" это все XML пробелы.
	 */
	public static final boolean isSpace(String s) {
		for (int i = 0; i < s.length(); i++) {
			if (!isSpace(s.charAt(i))) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Возвращает <i>true</i>, если символ <tt>c</tt> является членом <tt>S</tt> (space) [XML 1.1
	 * production 3].
	 *
	 * @param c Символ для проверки.
	 * @return <i>True</i>, если символ представляет собой XML пробелы.
	 */
	public static final boolean isSpace(char c) {
		// 0x20 = SPACE, 0x0A = LF, 0x0D = CR, 0x09 = TAB, 0x85 = NEL, 0x2028 = Line separator
		return 0x20 == c || 0x0A == c || 0x0D == c || 0x09 == c || 0x85 == c || 0x2028 == c;
	}

	// .........................................................................

	/**
	 * Внутренний класс, служит структурой, содержащей анализируемые компоненты ссылки.
	 */
	public static class Link {
		private String m_text;
		private String m_ref = null;
		private int m_interwikiPoint = -1;
		private List<Attribute> m_attribs = null;

		/**
		 * Создает новую ссылку с текстом, но без гиперссылки.
		 *
		 * @param text Текст ссылки.
		 * @throws ParseException Если текст ссылки некорректный.
		 */
		protected Link(String text) throws ParseException {
			setText(text);
		}

		/**
		 * Создает новую ссылку с заданным текстом и гиперссылкой (референс).
		 *
		 * @param text Текст ссылки.
		 * @param ref  Гипертекстовая ссылка.
		 * @throws ParseException Если текст ссылки или ссылка некорректный.
		 */
		protected Link(String text, String ref) throws ParseException {
			setText(text);
			setReference(ref);
		}

		/**
		 * Задает текст ссылки.
		 *
		 * @param text Текст ссылки.
		 * @throws ParseException Если текст ссылки некорректный. (напрмиер <code>null</code>).
		 */
		protected void setText(String text) throws ParseException {
			if (text == null) {
				throw new ParseException("null link text");
			}
			m_text = text;
		}

		/**
		 * Возвращает текст ссылки.
		 *
		 * @return Текст ссылки.
		 */
		public String getText() {
			return m_text;
		}

		/**
		 * Задает гипертекстовую ссылку. Как правило, это URI, или интервики-ссылка, или вики-ссылка.
		 *
		 * @param ref Гипертекстовая ссылка.
		 * @throws ParseException Если ссылка некорректна.
		 */
		protected void setReference(String ref) throws ParseException {
			if (ref == null) {
				throw new ParseException("null link reference value");
			}
			m_ref = ref;
		}

		/**
		 * Возвращает <i>true</i>, если есть гиперссылка.
		 *
		 * @return <i>True</i>, если есть гиперссылка; <i>False</i> в противном случае.
		 */
		public boolean hasReference() {
			return m_ref != null;
		}

		/**
		 * Возвращает гиперссылку или текст ссылки, если значение гиперссылки равно нулю.
		 *
		 * @return Референс ссылки.
		 */
		public String getReference() {
			return m_ref != null ? m_ref : m_text;
		}

		/**
		 * Возвращает <i>true</i>, если эта ссылка представляет собой ссылку InterWiki (в форме wiki:page).
		 *
		 * @return <i>True</i>, если эта ссылка представляет собой ссылку InterWiki.
		 */
		public boolean isInterwikiLink() {
			LinkParsingOperations lpo = new LinkParsingOperations();
			String reference = getReference();
			m_interwikiPoint = lpo.interWikiLinkAt(reference);
			return lpo.isInterWikiLink(reference);
		}

		/**
		 * Возвращает имя вики, если это интервики-ссылка.
		 *
		 * <pre>
		 *    Link link = new Link("Foo","Wikipedia:Foobar");
		 *    assert( link.getExternalWikiPage(), "Wikipedia" );
		 * </pre>
		 *
		 * @return Имя вики или значение <code>null</code>, если это не интервики-ссылка.
		 */
		public String getExternalWiki() {
			if (isInterwikiLink()) {
				return getReference().substring(0, m_interwikiPoint);
			}

			return null;
		}

		/**
		 * Возвращает часть вики-имени для интервики-ссылки. Используется только со ссылками на интервики.
		 *
		 * <pre>
		 *    Link link = new Link("Foo","Wikipedia:Foobar");
		 *    assert( link.getExternalWikiPage(), "Foobar" );
		 * </pre>
		 *
		 * @return Часть вики-имени или <code>null</code>, если это не интервики-ссылка.
		 */
		public String getExternalWikiPage() {
			if (isInterwikiLink()) {
				return getReference().substring(m_interwikiPoint + 1);
			}

			return null;
		}

		/**
		 * Возвращает количество атрибутов для этой ссылки.
		 *
		 * @return Число атрибутов.
		 */
		public int attributeCount() {
			return m_attribs != null ? m_attribs.size() : 0;
		}

		/**
		 * Добавляет еще один атрибут к ссылке.
		 *
		 * @param attr Атрибут JDOM.
		 */
		public void addAttribute(Attribute attr) {
			if (m_attribs == null) {
				m_attribs = new ArrayList<>();
			}
			m_attribs.add(attr);
		}

		/**
		 * Возвращает итератор по списку атрибутов JDOM.
		 *
		 * @return Итератор по атрибутам.
		 */
		public Iterator<Attribute> getAttributes() {
			return m_attribs != null ? m_attribs.iterator() : m_EMPTY.iterator();
		}

		/**
		 * Возвращает строковое представление викитекста этой ссылки.
		 *
		 * @return WikiText.
		 */
		@Override
		public String toString() {
			StringBuilder sb = new StringBuilder();
			sb.append('[');
			sb.append(m_text);

			if (m_ref != null) {
				sb.append(' ');
				sb.append('|');
				sb.append(' ');
				sb.append(m_ref);
			}

			if (m_attribs != null) {
				sb.append(' ');
				sb.append('|');
				Iterator<Attribute> it = getAttributes();
				while (it.hasNext()) {
					Attribute a = it.next();
					sb.append(' ');
					sb.append(a.getName());
					sb.append('=');
					sb.append('\'');
					sb.append(a.getValue());
					sb.append('\'');
				}
			}
			sb.append(']');
			return sb.toString();
		}

	}
	// end inner class

}
